package com.example.design.nioandnettty.单Reactor单线程;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOServer implements Runnable {
    //缓冲区大小
    private final int BUFFER_SIZE = 1024;
    //端口号
    private final int PORT = 8888;
    //缓冲区Buffer
    private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE);
    //多路复用器 NIO编程基础 负责管理通道Channel
    private Selector selector;

    public NIOServer() {
        startServer();
    }

    private void startServer() {
        try {
            //打开多路复用器
            selector = Selector.open();
            // 创建服务器通道(网络读写通道)
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //设置服务器通道模式为非阻塞模式，true为阻塞模式，false为非阻塞模式
            serverSocketChannel.configureBlocking(false);
            //绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));

            /**
             * SelectionKey.OP_READ     : 表示通道中已经有了可读的数据，可以执行读操作了（通道目前有数据，可以进行读操作了）
             * SelectionKey.OP_WRITE     : 表示已经可以向通道写数据了（通道目前可以用于写操作）
             * SelectionKey.OP_CONNECT: 表示客户与服务器的连接已经建立成功
             * SelectionKey.OP_ACCEPT : 表示服务器监听到了客户连接，服务器可以接收这个连接了
             */
            //把服务器通道注册到多路复用器上，并监听阻塞(OP_ACCEPT)事件。这个Channel一直在等待
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("Server satrt >>>>>>>>>>> port:" + PORT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //需要一个线程负责Selector的轮询
    //循环Client端注册到多路复用器的channel，看看这些channel的标志位都是什么状态
    @Override
    public void run() {
        while (true) {
            try {
                /**
                 * a.select() 阻塞到至少有一个通道在你注册的事件上就绪
                 * b.select(long timeOut) 阻塞到至少有一个通道在你注册的事件上就绪或者超时timeOut
                 * c.selectNow() 立即返回。如果没有就绪的通道则返回0
                 * select方法的返回值表示就绪通道的个数。
                 */
                //多路复用器监听阻塞，轮询就绪的Channel
                selector.select();
                //多路复用器已经选择的结果集（Channel集合）
                //SelectedKey是channel与Selector绑定的标记，每将一个channel注册到一个selector就会产生一个SelectedKey，
                //并将这个SelectedKey放入到Selected的key set中，注意，key set 只能通过这种方式添加，不可以直接添加，
                //但是可以手动移除。
                Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
                //不停的轮询
                while (selectionKeyIterator.hasNext()) {
                    //获取一个选中的key
                    SelectionKey next = selectionKeyIterator.next();
                    //获取后便将其从容器中移除,因为这个操作已经在下面处理完
                    //如果之后Channel又有数据了，就需要重新注册
                    selectionKeyIterator.remove();
                    //只获取有效的key  比如：Client与服务器端断掉了就不是有效的
                    if (!next.isValid()) {
                        continue;
                    }
                    //如果为阻塞状态,说明是新的客户端接入
                    if (next.isAcceptable()) {
                        accept(next);
                    }
                    //如果为可读状态,有新的就绪的数据包需要读取，则构造ByteBuffer对象，读取数据包；
                    if (next.isReadable()) {
                        read(next);
                    }
                    //如果读取数据的时候，客户端断网了，需要处理，不能在往下走，不然在isWritable()的时候
                    //抛出异常导致服务端直接停了
                    if (!next.isValid()) {
                        continue;
                    }
                    //如果为可写状态,说明还有数据没有发送完成，需要继续发送
                    if (next.isWritable()) {
                        System.out.println("可写...");
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    // 设置阻塞，等待Client请求。在传统IO编程中，用的是ServerSocket和Socket。
    // 在NIO中采用的ServerSocketChannel和SocketChannel
    private void accept(SelectionKey selectionKey) {
        try {
            //获取服务器通道
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
            //执行阻塞方法,调用ServerSocketChannel.accept()方法，创建客户端通道
            SocketChannel socketChannel = serverSocketChannel.accept();
            //设置服务器通道为非阻塞模式，true为阻塞，false为非阻塞
            socketChannel.configureBlocking(false);
            //把通道注册到多路复用器上并设置读取标识
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("有新的客户端接入: LocalAddress:" + socketChannel.getLocalAddress() + " RemoteAddress: " + socketChannel.getRemoteAddress());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void read(SelectionKey selectionKey) {
        SocketChannel socketChannel = null;
        try {
            // 清空缓冲区数据
            readBuffer.clear();
            // 获取在多路复用器上注册的通道
            socketChannel = (SocketChannel) selectionKey.channel();
            // 读取数据，返回
            int count = socketChannel.read(readBuffer);
            // 返回内容为-单Reactor单线程 表示没有数据
            if (-1 == count) {
                selectionKey.channel().close();
                selectionKey.cancel();
                return;
            }
            // 有数据则在读取数据前进行复位操作
            readBuffer.flip();
            // 根据缓冲区大小创建一个相应大小的bytes数组，用来获取值
            byte[] bytes = new byte[readBuffer.remaining()];
            // 接收缓冲区数据
            readBuffer.get(bytes);
            // 打印获取到的数据
            System.out.println("NIO Server : " + new String(bytes)); // 不能用bytes.toString()
        } catch (IOException e) {
            e.printStackTrace();
            try {
                //如果已经断开连接的(网络因素，或者人为的停了客户端)
                //需要将Channel关闭，否则会一直循环抛出异常
                socketChannel.close();
                //请求取消此键的通道到其选择器的注册
                selectionKey.cancel();
            } catch (IOException e1) {
                e1.printStackTrace();
            }

        }
    }

    public static void main(String[] args) {
        new Thread(new NIOServer()).start();
    }
}

//服务端的通道和客户端的通道都会注册到selector上与SelectionKey关联起来。
//只不过服务端通道监听阻塞(OP_ACCEPT)事件，说明服务器通道用于监听指定的端口，接受客户端的连接请求
//客户端通道通常关注的是读事件（SelectionKey.OP_READ）和写事件（SelectionKey.OP_WRITE）。
//这里的示例客户端只监听了SelectionKey.OP_READ事件，写的事件也没写。
////如果为可写状态,说明还有数据没有发送完成，需要继续发送
//if(next.isWritable()){
//    System.out.println("可写...");
//}
// selectionKey将Channel与Selector关联起来，并维护了channel事件的状态。
// listenChannel.accept(); 服务端通道调accept方法创建客户端通道，然后在把客户端通道注册道Selector上